package ch.ethz.inf.vs.sunspot.drivercore;

import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import ch.ethz.inf.vs.sunspot.Constants;

/**
 * This class represents a Sun SPOT and handles 
 * encoding and decoding of messages (the counterpart of this class is on the SPOT itself)
 * 
 * @author Simon Mayer, ETH Zurich
 *
 */
public class SpotRepresentation {
	
	// Java logging instance
	private static Logger log = Logger.getLogger("SpotRepresentation");

	// SPOT Representations, oldMeAsJSON used to check if information has to be sent
	JSONObject meAsJSON;
	JSONObject oldMeAsJSON;

	// Management Information: Does the Interval time have to be reloaded
	private Boolean reload;
	
	// Age of the data retrieved	
	private Long dataTime;	

	// Sensor Data
	private int lightValue;
	private double temperatureValue;
	private LedColor[] ledColors;
	private boolean[] ledStatuses;
	private boolean[] switchStatuses;
    private double[] accelerationValues;
    private double[] tiltValues;
    private double[] adcValues;
    
	// Actuator Data, changes here will be passed on to the SPOT during the next synchronization round
	private LedColor[] ledColorFuture;
	private boolean[] ledStatusFuture;

	// Information on the SPOT that is represented
	private int interval;
	private String name;
	
	/**
	 * Create a new Spot Representation and parse it from a creator-string
	 */
	public SpotRepresentation(String name) {
		log.setLevel(Constants.LOGLEVEL);

		this.name = name;

		// Initialize Sensors
		this.lightValue = 0;
		this.temperatureValue = 0.0;
		this.ledColors = new LedColor [SPOT_NUMBER_AVAILABLE_LEDS]; // int[Constants.SPOT_NUMBER_AVAILABLE_LEDS][3];
		this.ledStatuses = new boolean[SPOT_NUMBER_AVAILABLE_LEDS];
		this.switchStatuses = new boolean[2];
        this.accelerationValues = new double[3];
        this.tiltValues = new double[3];
        this.adcValues = new double[6];
        
		// Initialize Actuators
		this.ledColorFuture = new LedColor [SPOT_NUMBER_AVAILABLE_LEDS]; // int[Constants.SPOT_NUMBER_AVAILABLE_LEDS][3];
		this.ledStatusFuture = new boolean[SPOT_NUMBER_AVAILABLE_LEDS];

		// Initialize LedColor Objects
		for (int i = 0; i < SPOT_NUMBER_AVAILABLE_LEDS; i++) {
			this.ledColors[i] = new LedColor();
			this.ledColorFuture[i] = new LedColor(255, 255, 255);
			this.ledStatuses[i] = false;
		}

		// Initialize Management
		this.dataTime = System.currentTimeMillis();
		this.reload = false;

		this.interval = Constants.SPOT_SYNC_INTERVAL;

		// Initialize Representation
		this.meAsJSON = new JSONObject();
	}

	/**
	 * Encode the current object to a JSON String (in order to pass it to the SPOT) while including the given re-synchronization time
	 * @param nextResyncTime
	 * @return the object, encoded as JSON String
	 */
	public String encodeObject_OutToSpot(Integer nextResyncTime) {
		if (oldMeAsJSON == null) {
			log.log(Level.INFO, "Created new OLDJSON object");
			oldMeAsJSON = new JSONObject();
		}

		meAsJSON = new JSONObject();

		// Better do this unconditionally...
		try {
			meAsJSON.put(SPOT_INTERVALFUTURE_NAME, nextResyncTime);
		} catch (JSONException e) {
			e.printStackTrace();
		}

		for (int i = 0; i < SPOT_NUMBER_AVAILABLE_LEDS; i++) {
			JSONArray thisLED = new JSONArray();
			thisLED.put(this.ledStatusFuture[i] ? "1" : "0");

			thisLED.put("" + this.ledColorFuture[i].getRed());
			thisLED.put("" + this.ledColorFuture[i].getGreen());
			thisLED.put("" + this.ledColorFuture[i].getBlue());

			try {
				// Test whether to reload the actuator data and, if yes, load it into the JSON object
				if (this.reload || !oldMeAsJSON.has(SPOT_LEDFUTURE_NAME + i) || !oldMeAsJSON.getJSONArray(SPOT_LEDFUTURE_NAME + i).toString().equalsIgnoreCase(thisLED.toString())) {
					meAsJSON.put(SPOT_LEDFUTURE_NAME + i, thisLED);
					oldMeAsJSON.put(SPOT_LEDFUTURE_NAME + i, thisLED);
				}

				// If the spot's LED information differs from the local future information, send anyway - this is to relay actuator changes even if the SPOT loses its connection!
				if (meAsJSON.has(SPOT_LEDFUTURE_NAME + i) && meAsJSON.has(SPOT_LED_NAME + i) && !meAsJSON.getJSONArray(SPOT_LEDFUTURE_NAME + i).toString().equalsIgnoreCase(meAsJSON.getJSONArray(SPOT_LED_NAME + i).toString())) {
					meAsJSON.put(SPOT_LEDFUTURE_NAME + i, thisLED);
					oldMeAsJSON.put(SPOT_LEDFUTURE_NAME + i, thisLED);
				}
			} catch (JSONException ex) {
				ex.printStackTrace();
			}
		}

		this.reload = false;
		return MESSAGE_SPOT_RESYNC + SEPARATOR + meAsJSON.toString();
	}

	/**
	 * Encode the current object to a JSON String (in order to pass it to the Client)
	 * @return the object, encoded as JSON String
	 */
	public String encodeObject_OutToClient() {
		meAsJSON = new JSONObject();

		try {
			meAsJSON.put(SPOT_INTERVALFUTURE_NAME_EXTERNAL, getResyncTime());
		} catch (JSONException e) {
			e.printStackTrace();
		}

		try {
			meAsJSON.put(SPOT_LIGHT_NAME_EXTERNAL, "" + getLightValue());
			meAsJSON.put(SPOT_TEMPERATURE_NAME_EXTERNAL, "" + getTemperatureValue());
		} catch (JSONException ex) {
			ex.printStackTrace();
		}


		for (int i = 0; i < SPOT_NUMBER_AVAILABLE_LEDS; i++) {
			JSONArray thisLED = new JSONArray();
			JSONArray thisLEDFuture = new JSONArray();

			thisLED.put(this.ledStatuses[i] ? "1" : "0");
			thisLEDFuture.put(this.ledStatusFuture[i] ? "1" : "0");

			thisLED.put("" + this.ledColors[i].getRed());
			thisLED.put("" + this.ledColors[i].getGreen());
			thisLED.put("" + this.ledColors[i].getBlue());

			thisLEDFuture.put("" + this.ledColorFuture[i].getRed());
			thisLEDFuture.put("" + this.ledColorFuture[i].getGreen());
			thisLEDFuture.put("" + this.ledColorFuture[i].getBlue());

			try {
				meAsJSON.put(SPOT_LED_NAME_EXTERNAL + i, thisLED);
				meAsJSON.put(SPOT_LEDFUTURE_NAME_EXTERNAL + i, thisLEDFuture);
			} catch (JSONException ex) {
				ex.printStackTrace();
			}
		}

		try {
			meAsJSON.put(SPOT_SWITCH_NAME_EXTERNAL + 0, this.switchStatuses[0] ? "1" : "0");
			meAsJSON.put(SPOT_SWITCH_NAME_EXTERNAL + 1, (this.switchStatuses[1] ? "1" : "0"));
		} catch (JSONException ex) {
			ex.printStackTrace();
		}
		
		JSONArray thisAcceleration = new JSONArray();
		JSONArray thisTilt = new JSONArray();
		JSONArray thisADC = new JSONArray();
			
		for (int i = 0; i < 3; i++) thisAcceleration.put("" + this.accelerationValues[i]);			
		for (int i = 0; i < 3; i++) thisTilt.put("" + this.tiltValues[i]);
		for (int i = 0; i < 6; i++) thisADC.put("" + this.adcValues[i]);
		
		try {
			meAsJSON.put(SPOT_ACCELERATION_NAME_EXTERNAL, thisAcceleration);
			meAsJSON.put(SPOT_TILT_NAME_EXTERNAL, thisTilt);
			meAsJSON.put(SPOT_ADC_NAME_EXTERNAL, thisADC);
		} catch (JSONException ex) {
			ex.printStackTrace();
		}
		
		return meAsJSON.toString();
	}

	/**
	 * Encode the current object to a JSON String (in order to pass it to the Client)
	 * @return the object, encoded as JSON String
	 */
	public String encodeObject_outToAtom() {
		meAsJSON = new JSONObject();

		try {
			meAsJSON.put(SPOT_LIGHT_NAME_EXTERNAL, "" + getLightValue());
			meAsJSON.put(SPOT_TEMPERATURE_NAME_EXTERNAL, "" + getTemperatureValue());
		} catch (JSONException ex) {
			ex.printStackTrace();
		}


		for (int i = 0; i < SPOT_NUMBER_AVAILABLE_LEDS; i++) {
			JSONArray thisLED = new JSONArray();

			thisLED.put(this.ledStatuses[i] ? "1" : "0");

			thisLED.put("" + this.ledColors[i].getRed());
			thisLED.put("" + this.ledColors[i].getGreen());
			thisLED.put("" + this.ledColors[i].getBlue());

			try {
				meAsJSON.put(SPOT_LED_NAME_EXTERNAL + i, thisLED);
			} catch (JSONException ex) {
				ex.printStackTrace();
			}
		}

		try {
			meAsJSON.put(SPOT_SWITCH_NAME_EXTERNAL + 0, this.switchStatuses[0] ? "1" : "0");
			meAsJSON.put(SPOT_SWITCH_NAME_EXTERNAL + 1, (this.switchStatuses[1] ? "1" : "0"));
		} catch (JSONException ex) {
			ex.printStackTrace();
		}
		
		JSONArray thisAcceleration = new JSONArray();
		JSONArray thisTilt = new JSONArray();
		JSONArray thisADC = new JSONArray();	
		
		for (int i = 0; i < 3; i++) thisAcceleration.put("" + this.accelerationValues[i]);			
		for (int i = 0; i < 3; i++) thisTilt.put("" + this.tiltValues[i]);
		for (int i = 0; i < 6; i++) thisADC.put("" + this.adcValues[i]);

		try {
			meAsJSON.put(SPOT_ACCELERATION_NAME_EXTERNAL, thisAcceleration);
			meAsJSON.put(SPOT_TILT_NAME_EXTERNAL, thisTilt);
			meAsJSON.put(SPOT_ADC_NAME_EXTERNAL, thisADC);
		} catch (JSONException ex) {
			ex.printStackTrace();
		}
		
		return meAsJSON.toString();
	}
	
	/**
	 * Decode the creator-string to a JSON object (for strings originating from a client of the driver)
	 * @param creator, the String to create a new JSON object from
	 */
	public void decodeToObjectFuture_inFromClient(String creator) { 
		try {
			meAsJSON = new JSONObject(creator);
		} catch (JSONException e) {
			e.printStackTrace();
		}

		log.fine(creator);

		if (meAsJSON.has(SPOT_INTERVALFUTURE_NAME_EXTERNAL)) {
			try {
				this.interval = meAsJSON.getInt(SPOT_INTERVALFUTURE_NAME_EXTERNAL);
				log.fine("New interval: " + this.interval);

				SpotManagement.getInstance().setFutureSyncTimeout(this.name, this.interval);
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}

		for (int i = 0; i < SPOT_NUMBER_AVAILABLE_LEDS; i++) {
			if (meAsJSON.has(SPOT_LEDFUTURE_NAME_EXTERNAL + i)) {
				try {
					JSONArray leds = meAsJSON.getJSONArray(SPOT_LEDFUTURE_NAME_EXTERNAL + i);
					this.ledStatusFuture[i] = (leds.getInt(0) == 1);

					this.ledColorFuture[i].setRed(leds.getInt(1));
					this.ledColorFuture[i].setGreen(leds.getInt(2));
					this.ledColorFuture[i].setBlue(leds.getInt(3));
				} catch (JSONException e) {
					e.printStackTrace();
				}
			}
			// TODO: Currently doing both...
			else if (meAsJSON.has(SPOT_LEDFUTURE_NAME + i)) {
				try {
					JSONArray leds = meAsJSON.getJSONArray(SPOT_LEDFUTURE_NAME + i);
					this.ledStatusFuture[i] = (leds.getInt(0) == 1);

					this.ledColorFuture[i].setRed(leds.getInt(1));
					this.ledColorFuture[i].setGreen(leds.getInt(2));
					this.ledColorFuture[i].setBlue(leds.getInt(3));
				} catch (JSONException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * Decode the creator-string to a JSON object (for strings originating from the SPOT) 
	 * @param creator, the String to create a new JSON object from
	 */
	public void decodeToObject_inFromSpot(String creator) {
		this.setDataTime(System.currentTimeMillis());

		try {
			meAsJSON = new JSONObject(creator);
		} catch (JSONException e) {
			e.printStackTrace();
		}

		if (meAsJSON.has(SPOT_INTERVALFUTURE_NAME)) {
			try {
				setResyncTime(meAsJSON.getInt(SPOT_INTERVALFUTURE_NAME));
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}

		if (meAsJSON.has(SPOT_LIGHT_NAME)) {
			try {
				this.lightValue = meAsJSON.getInt(SPOT_LIGHT_NAME);
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}

		if (meAsJSON.has(SPOT_TEMPERATURE_NAME)) {
			try {
				this.temperatureValue = meAsJSON.getDouble(SPOT_TEMPERATURE_NAME);
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}

		for (int i = 0; i < SPOT_NUMBER_AVAILABLE_LEDS; i++) {
			if (meAsJSON.has(SPOT_LED_NAME + i)) {
				try {
					JSONArray leds = meAsJSON.getJSONArray(SPOT_LED_NAME + i);
					this.ledStatuses[i] = (leds.getInt(0) == 1);

					this.ledColors[i].setRed(leds.getInt(1));
					this.ledColors[i].setGreen(leds.getInt(2));
					this.ledColors[i].setBlue(leds.getInt(3));
				} catch (JSONException e) {
					e.printStackTrace();
				}
			}
		}

		if (meAsJSON.has(SPOT_SWITCH_NAME + 0)) {
			try {
				this.switchStatuses[0] = (meAsJSON.getInt(SPOT_SWITCH_NAME + 0) == 1);
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}

		if (meAsJSON.has(SPOT_SWITCH_NAME + 1)) {
			try {
				this.switchStatuses[1] = (meAsJSON.getInt(SPOT_SWITCH_NAME + 1) == 1);
			} catch (JSONException e) {
				e.printStackTrace();
			}
		}
		
		if (meAsJSON.has(SPOT_ACCELERATION_NAME)) {
			try {
				JSONArray accelerationValues = meAsJSON.getJSONArray(SPOT_ACCELERATION_NAME);
			
				this.accelerationValues[0] = accelerationValues.getDouble(0);
				this.accelerationValues[1] = accelerationValues.getDouble(1);
				this.accelerationValues[2] = accelerationValues.getDouble(2);
			} catch (JSONException e) {
				e.printStackTrace();
			}			
		}
		
		if (meAsJSON.has(SPOT_TILT_NAME)) {
			try {
				JSONArray tiltValues = meAsJSON.getJSONArray(SPOT_TILT_NAME);
			
				this.tiltValues[0] = tiltValues.getDouble(0);
				this.tiltValues[1] = tiltValues.getDouble(1);
				this.tiltValues[2] = tiltValues.getDouble(2);
			} catch (JSONException e) {
				e.printStackTrace();
			}			
		}
		
		if (meAsJSON.has(SPOT_ADC_NAME)) {
			try {
				JSONArray adcValues = meAsJSON.getJSONArray(SPOT_ADC_NAME);
			
				for (int i = 0; i < 6; i++) {
					this.adcValues[i] = adcValues.getDouble(i);	
				}
			} catch (JSONException e) {
				e.printStackTrace();
			}			
		}
	}
	
	// GETTERS and SETTERS
	public int getLightValue() {
		return this.lightValue;
	}

	public void setLightValue(int lightValue) {
		this.lightValue = lightValue;
	}

	public void setResyncTime(int resyncTime) {
		this.interval = resyncTime;
	}

	public int getResyncTime() {
		return this.interval;
	}

	public double getTemperatureValue() {
		return this.temperatureValue;
	}

	public void setTemperatureValue(double temperatureValue) {
		this.temperatureValue = temperatureValue;
	}

	public boolean getLedStatus(int ledIndex) {
		return this.ledStatuses[ledIndex];
	}

	public void setLedStatus(int ledIndex, boolean ledStatus) {
		this.ledStatuses[ledIndex] = ledStatus;
	}

	public int [] getLedColor(int ledIndex) {
		return this.ledColors[ledIndex].getAllColors();
	}

	public LedColor accessLedColorFuture(int ledIndex) {
		return this.ledColorFuture[ledIndex];
	}

	public void setLedColor(int ledIndex, int [] ledColors) {
		this.ledColors[ledIndex] = new LedColor(ledColors);
	}

	public boolean getSwitchStatus(int switchIndex) {
		return this.switchStatuses[switchIndex];
	}

	public void setSwitchStatus(int switchIndex, boolean switchStatus) {
		this.switchStatuses[switchIndex] = switchStatus;
	}
	
    public double[] getAccelerationValues() {
        return this.accelerationValues;
    }

    public void setAccelerationValues(double[] accelerationValues) {
        this.accelerationValues = accelerationValues;
    }

    public double[] getTiltValues() {
        return this.tiltValues;
    }

    public void setTiltValues(double[] tiltValues) {
        this.tiltValues = tiltValues;
    }

    public double[] getADCValues() {
        return this.adcValues;
    }

    public void setADCValues(double[] adcValues) {
        this.adcValues = adcValues;
    }
    
	public int [] getLedColorFuture(int ledIndex) {
		return ledColorFuture[ledIndex].getAllColors();
	}

	public void setLedColorFuture(int ledIndex, int [] ledColorFuture) {
		this.ledColorFuture[ledIndex] = new LedColor(ledColorFuture);
	}

	public boolean getLedStatusFuture(int ledIndex) {
		return ledStatusFuture[ledIndex];
	}

	public void setLedStatusFuture(int ledIndex, boolean ledStatusFuture) {
		this.ledStatusFuture[ledIndex] = ledStatusFuture;
	}

	public void setDataTime(Long dataTime) {
		this.dataTime = dataTime;
	}

	public Long getDataTime() {
		return dataTime;
	}

	public void setReload() {
		this.reload = true;
	}
	
	public static Integer SPOT_NUMBER_AVAILABLE_LEDS = 7;
	
	// Tags for the JSON encoding used in communication between the Host and the SPOTs
	public static final String SPOT_LIGHT_NAME = "I";
	public static final String SPOT_TEMPERATURE_NAME = "T";
	public static final String SPOT_LED_NAME = "E";
	public static final String SPOT_SWITCH_NAME = "S";
	public static final String SPOT_ACCELERATION_NAME = "A";
    public static final String SPOT_TILT_NAME = "L";
    public static final String SPOT_ADC_NAME = "D";
    public static final String SPOT_LEDFUTURE_NAME = "F";
	public static final String SPOT_INTERVALFUTURE_NAME = "N";
	public static final String USER_TOKEN = "USERTOKEN_";
	
	// Tags for the JSON encoding used in communication between the Host and Clients over HTTP
	public static final String SPOT_LIGHT_NAME_EXTERNAL = "valuelight";
	public static final String SPOT_TEMPERATURE_NAME_EXTERNAL = "valuetemperature";
	public static final String SPOT_LED_NAME_EXTERNAL = "valueled";
	public static final String SPOT_SWITCH_NAME_EXTERNAL = "valueswitch";
	public static final String SPOT_ACCELERATION_NAME_EXTERNAL = "valueacceleration";
    public static final String SPOT_TILT_NAME_EXTERNAL = "valuetilt";
    public static final String SPOT_ADC_NAME_EXTERNAL = "valueadc";
	public static final String SPOT_LEDFUTURE_NAME_EXTERNAL = "valuefutureled";
	public static final String SPOT_INTERVALFUTURE_NAME_EXTERNAL = "interval";
	
	// Message headers
	public static String MESSAGE_SPOT_RESYNC = "R";
	public static String MESSAGE_SPOT_SYNC = "S";
	public static String MESSAGE_SPOT_HELO = "H";
	public static String SEPARATOR = ":";
	public static String YOU_ARE_BLOCKED = "B";

	public String getSpotName() {
		return this.name;
	}
}
